Skip to content

Release 1.1.0 - #237

Merged
FlorianRappl merged 34 commits into
mainfrom
devel
Sep 5, 2026
Merged

Release 1.1.0#237
FlorianRappl merged 34 commits into
mainfrom
devel

Conversation

@FlorianRappl

Copy link
Copy Markdown
Contributor

Types of Changes

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

Thanks to all the contributors in this one we have fixed a set of bugs and added some great enhancements. Most importantly, we improved media queries and fixed some CSS computation bugs w.r.t. media features / types, as well as in the CSS variables (calc) space.

Through the new IRenderDevicePreferences interface we are capable of dynamically adding platform descriptors, which are then used throughout the CSS evaluation (right now exclusive to media computations).

lahma and others added 18 commits September 2, 2026 18:40
CssMediaQueryList.ComputeMatched returned a constant false, so
window.matchMedia(...).IsMatched answered false for every query,
including "all" and the empty query, which always match. It now
validates the media list against the render device from the browsing
context, reusing the very same evaluation that @media rules already go
through for the cascade, and falls back to DefaultRenderDevice when no
device is registered - the same fallback GetComputedStyle uses.

Reported in AngleSharp/AngleSharp#1307.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
CssColorValue always serializes through rgba(), so an opaque color comes
out as rgba(r, g, b, 1) where the CSSOM serialization rules ask for
rgb(r, g, b). Changing that by default would be breaking, so this adds
UseSpecSerialization next to UseHex: off by default, and when switched
on an opaque color serializes as rgb(r, g, b) while anything with an
alpha below 1 keeps rgba(r, g, b, a). UseHex still wins when both are
active.

Reducing a color that was written as a named color back to its name is a
separate step and is not part of this change.

Closes #227.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Evaluate the media query list in matchMedia against the render device
Add an opt-in switch for CSSOM-compliant color serialization
Adds IRenderDevicePreferences, a small interface with a single
IReadOnlyDictionary<String, String> Preferences member that
DefaultRenderDevice implements, so a host can say which user preferences
its device carries without every existing IRenderDevice implementation
having to change.

A generic PreferenceFeatureValidator is registered for
prefers-color-scheme, prefers-reduced-motion,
prefers-reduced-transparency, prefers-contrast, prefers-reduced-data,
forced-colors and display-mode, and hover/any-hover and
pointer/any-pointer now read the dictionary when it carries them, while
keeping their previous answer when it does not. A key that is not set
leaves its feature unknown, i.e., the query does not match.

Fixes #234

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Review feedback on #235: the convenience layer over IRenderDevice is
useful to hosts that configure a DefaultRenderDevice, so the class is
public and documented.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Answer the user-preference media features from the render device
The calc add/sub/mul/div expressions create the resulting metric value via
Activator.CreateInstance(x.GetType(), result). The trimmer cannot see that
call target, so the single-Double constructors of the built-in metric values
were removed, making every calc() computation throw MissingMethodException in
NativeAOT (and trimmed) applications.

The reflection call now lives in a single helper that roots the public
constructors of all built-in metric values via DynamicDependency, so the
behavior matches the JIT one. ICssMetricValue is public, hence external
implementations stay supported - they just have to preserve their own
constructor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a2612f6f-7d43-40de-a31f-96a2600e6f81
Fix calc() computation in trimmed / NativeAOT applications
@FlorianRappl FlorianRappl added this to the v1.1.0 milestone Sep 4, 2026
meziantou and others added 10 commits September 4, 2026 20:50
CssTokenizer.ContentFrom re-scans the raw source to recover a declaration
value or an at-rule prelude, and breaks at the first ';', '{' or '}'. It
special-cased quoted strings but knew nothing about url tokens, where all
three characters are legal content.

As a result "url(data:image/svg+xml;base64,...)" was cut at the first
semicolon and became url("data:image/svg+xml"); the remainder failed to
re-tokenize as a declaration and was dropped, so a round-trip through
CssText silently destroyed the asset. Unquoted data URIs are emitted by
every major bundler, so this affected many real stylesheets. Via the
GetArgument path the same flaw discarded whole rules: a @supports
condition containing url(a;b) lost its entire rule.

Teach ContentFrom about url tokens: on an ident "url" immediately followed
by '(', consume through the matching unescaped ')' before the break check
resumes. Behaviour was validated against Chrome's CSSOM over ~40 inputs,
which pinned down three subtleties:

- The ident must be immediately followed by '(' - "myurl(", "-url(" and
  "url\t(" are ordinary function tokens where ';' does terminate the
  declaration, so a plain substring match would introduce a new bug.
- url( followed by a quote is a function token, not a url token: the
  string wins and a ')' inside it does not close the url. The scan skips
  whitespace after '(' and defers to the existing string handling.
- Bad-url cases such as url(a b) still consume through the matching ')',
  so a single "consume to unescaped ')'" rule extracts the correct span
  in every case.

Escapes inside the url are honoured, so url(a\;b) also parses correctly
where it previously produced url("a\\").

The remaining divergences from Chrome (url(a b), url(a(b), url()) live in
UrlUQ/UrlBad and are unchanged by this commit.
The @supports condition parser compared the "and" / "or" keywords with
ordinal equality, so an uppercase or mixed-case keyword silently
discarded the whole conditional group and every rule inside it. The
keyframe selector parser did the same for "from" / "to", leaving the
rule in the CSSOM with a null key.

Switched both to the case-insensitive Isi helper, including the chain
continuation in Scan, which compared each subsequent keyword against
the raw text of the first one.
The calc() operand parser recursed into itself for the right operand of
every operator, which made the resulting expression tree right
associative. Chains of two or more identical operators were therefore
evaluated in the wrong order: calc(10px - 2px - 3px) built
Sub(10px, Sub(2px, 3px)) and computed to 11px instead of 5px, and
calc(100px / 2 / 5) built 100 / (2 / 5) instead of (100 / 2) / 5.

Mixed precedence expressions happened to come out right, so only chains
of same precedence operators were affected. No parse error was raised;
the wrong number was simply handed to the computed style.

Replace the four right recursive levels with two iterative loops that
fold the operands to the left, matching the grammar

    expression := term (('+' | '-') term)*
    term       := factor (('*' | '/') factor)*

This also collapses the artificial split between the Add/Sub and the
Mul/Div levels, which is what introduced the asymmetry. Serialization is
unaffected, as CssText concatenates the operands in order without
adding parentheses.

Expected values are taken from Chrome via getComputedStyle.
Two independent defects made a division in calc() report a value with
the wrong unit.

Dividing two values that share a unit cancels the unit out and yields a
plain number, but CssCalcDivExpression kept the unit of the left
operand. calc(10px / 20px) computed to 0.5px instead of 0.5, so
declarations such as opacity, flex-grow, z-index or line-height ended up
with a length where a number was expected.

CssMetricValueExtensions.WithValue creates the result through
Activator.CreateInstance(type, value), and the single argument
constructor of CssLengthValue defaults to pixels. Any unitless length
was therefore turned into a length in pixels: calc(1 / 4) computed to
0.25px rather than 0.25. Preserve the unit of the template instead; this
covers multiplication too, where calc(2 * 3) computed to 6px.

Expected values are taken from Chrome via getComputedStyle, which
reports 0.5 for opacity: calc(10px / 20px), 2 for
flex-grow: calc(100px / 50px) and 150px for
width: calc(100px / 2px * 3px).
My previous commit fixed where a url token *ends*. This fixes what happens
when one is invalid, which was a separate defect with the same symptom
class: values that browsers reject were being accepted as garbage.

Three divergences from Chrome, all rooted in the fact that a bad url had
no way to be reported as a failure:

CssUriParser.Bad() returned a CssUrlValue built from whatever characters
it had scanned past, so an invalid url produced a plausible-looking but
wrong value instead of failing. url(a b) became url("ab") and
url(a(b) became url("a(b)"); browsers drop the declaration in both cases.
Bad() now returns null and ParseUri rewinds the source, so the url() is
seen as unparsed rather than as absent - rewinding matters, because merely
consuming the bad url let "background: url(a b) red" silently re-parse as
"background: red" instead of being dropped.

ParseUri did not consume the ')' of an empty url(), leaving the source
mid-value so the declaration was rejected. url() is valid and means the
empty URL, so it now parses as url("").

CssTokenizer.NewUrl accepted a "bad" parameter and ignored it, so no
bad-url token could ever exist at the sheet level and UrlBad's scanned-over
characters became the url's content. "@import url(a b)" imported the
garbage href "a  b)". A BadUrl token type now carries the distinction, and
UrlBad discards the remnants it consumes. Per the spec, EOF ends a url
token rather than invalidating it, so the two EOF paths that flagged bad
no longer do - "@import url(abc" still imports "abc".

Consequences at the rule level, matching Chrome:

- @import with a bad url is dropped instead of importing a garbage href.
- @namespace with a bad url is dropped. Fixing this forced a decision on
  the string form, since one condition governs both: @namespace accepted
  only a url token, so "@namespace x "http://foo"" silently produced an
  empty namespace URI. It now accepts a string as the spec requires.

Behaviour was verified against Chrome's CSSOM over the full 30-case matrix
(both fixes together); no divergence remains.

ParseInlineStyleWithToleratedInvalidValueShouldReturnThatValue asserted the
old lenient recovery for url(javascript:alert(1)) - an unquoted url with a
'(' in it, i.e. exactly the url(a(b) case. Chrome drops that declaration,
so the test now documents that, and a companion test covers the quoted form
which is valid and still round-trips. The tolerance it relied on came from
Bad(), not from IsIncludingUnknownDeclarations, which only governs unknown
property names.
Resolve per-element custom-property dependency components iteratively before inheritance, including unused fallback edges. Preserve computed token values, substitute complete consumer and shorthand values, and apply computed-value defaults without recursive variable evaluation.

Add regression coverage for cycles, inheritance, shared rules, CSSOM mutation, token boundaries, long chains, nested fallbacks, and bounded expansion. Document the substitution limit.

Fixes #241

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…sociativity-96200d

Fix operator associativity and result units in calc()
…tivity-c30828

Fix case-sensitive matching of and/or and from/to keywords in CSS parsers
…-68b99b

Fix unquoted and invalid url() handling
FlorianRappl and others added 6 commits September 5, 2026 14:37
Keep variable resolution inside computed-style preparation; leave raw cascades and render-tree specified styles unchanged. Restore nested CssVarValue fallback trees and direct References-based computation, retaining iterative parsing, serialization, and fallback traversal.

Honor caller-modified references in custom-property and shorthand resolution. Add compatibility regressions for raw declarations, public parser cursor and value-tree behavior, mutable references, and deep public fallbacks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fix circular custom properties during computed style resolution
@FlorianRappl
FlorianRappl merged commit ae8b2a3 into main Sep 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants